#include "texteditor.h"
#include "autocompleter.h"

#include <QKeyEvent>
#include <QAbstractItemView>
#include <QtDebug>
#include <QApplication>
#include <QModelIndex>
#include <QAbstractItemModel>
#include <QScrollBar>
#include <QStringListModel>
#include <QPainter>
#include <QTextBlock>
#include <QAbstractTextDocumentLayout>
#include <QFile>
#include <QPalette>
#include <QFontMetrics>
#include "MainWindow/mainwindow.h"
#include "Indenter/cstyleindenter.h"
#include "Indenter/pythonindenter.h"
#include "Indenter/htmlindenter.h"
#include "Indenter/shbashindenter.h"
#include "Indenter/fortranindenter.h"
#include "Change/textchange.h"
#include "WelcomeScreen/welcomescreen.h"

/**
  * Create text editor and connect signals for advanced editing features.
  */
TextEditor::TextEditor(DocumentManager *newManager) : Editor(newManager)
{
	lineNumberArea = new LineNumberArea(this);
	indenter = NULL;
	programmingLanguage = NONE;
	completer = NULL;
	setLineNumbers(false);

	connect(document(), SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth()));
	connect(this, SIGNAL(textChanged()), lineNumberArea, SLOT(repaint()));
	connect(MainWindow::getInstance(), SIGNAL(moved()), this, SLOT(windowMoved()));
	connect(document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(textChanged(int,int,int)));

	//connect to preferences and set initial ones
	connect(Preferences::getInstance(), SIGNAL(changed()), this, SLOT(preferencesChanged()));
	preferencesChanged();

	connect(verticalScrollBar(), SIGNAL(valueChanged(int)), lineNumberArea, SLOT(repaint()));

	updateLineNumberAreaWidth();
}


/**
  *	Delivers a new change from the server (via the session and document manager)
  */
void TextEditor::receiveChange(QByteArray change)
{
	document()->blockSignals(true);

	if (Message::getOpType(change) == Message::ADD_TEXT)
	{
		TextChange txtChg(change);

		if (txtChg.getUserId() != documentManager->getLocalUserId())
		{
			QTextCursor cursor(document());
			cursor.setPosition(txtChg.getPosition());

			//set user highlighting format
			QTextCharFormat userNumberFormat;
			userNumberFormat.setProperty(QTextFormat::UserProperty, QVariant(txtChg.getUserId() + 1));
			cursor.mergeCharFormat(userNumberFormat);

			cursor.insertText(txtChg.getData());
		}

		changeList.push_back((Change)txtChg);
	}

	else if (Message::getOpType(change) == Message::REMOVE)
	{
		TextChange txtChg(change);

		if (txtChg.getUserId() != documentManager->getLocalUserId())
		{
			QTextCursor cursor(document());
			cursor.setPosition(txtChg.getPosition());
			cursor.setPosition(txtChg.getPosition() + txtChg.getNum(), QTextCursor::KeepAnchor);
			cursor.removeSelectedText();
		}

		changeList.push_back((Change)txtChg);
	}

	highlighter->rehighlight();	//manually rehighlight the contents
	updateLineNumberAreaWidth();
	lineNumberArea->repaint();
	repaint();

	document()->blockSignals(false);
}

/**
  *	Sends a change to the server when the user changes something
  */
void TextEditor::textChanged(int position, int charsRemoved, int charsAdded)
{
	if (charsRemoved)
	{
		TextChange remChange(Change::REMOVE, revisionNum++, documentManager->getLocalUserId(), docId, position, charsRemoved, NULL);
		documentManager->sendChange(remChange.toBytes());
	}

	if (charsAdded)
	{
		//get the character/s
		QTextCursor cursor = textCursor();
		cursor.setPosition(position);
		cursor.setPosition(position + charsAdded, QTextCursor::KeepAnchor);
		//cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, charsAdded);

		QString data = cursor.selection().toPlainText();

		TextChange addChange(Change::ADD_TEXT, revisionNum++, documentManager->getLocalUserId(), docId, position, charsAdded, data);
		documentManager->sendChange(addChange.toBytes());

		document()->blockSignals(true);

		//fix up local highlighting
		QTextCharFormat userNumberFormat;
		userNumberFormat.setProperty(QTextFormat::UserProperty, QVariant(documentManager->getLocalUserId() + 1));
		cursor.mergeCharFormat(userNumberFormat);

		document()->blockSignals(false);
	}
}

/**
  * Fix the line number width depending on the number of digits needed.
  */
int TextEditor::updateLineNumberAreaWidth()
{
	if (showLineNumbers)
	{
		int newWidth = fontMetrics().width(QString::number(document()->blockCount())) + 3;

		setViewportMargins(newWidth, 0, 0, 0);

		return newWidth;
	}

	return 0;
}

/**
  * Paints the long line marker on the screen.
  */
void TextEditor::paintEvent(QPaintEvent *event)
{
	QPainter painter(viewport());

	// Fill in the cursor position so we know where it is when the box pops up.
	if (completer && completer->isVisible())
		painter.fillRect(cursorRect(), QPalette().color(QPalette::Base));

	// Paint long line marker to screen.
	if (longLinePos != -1)
	{
		painter.setPen(QColor(0, 255, 0));

		// We use + 4 because it's in the middle of the previous char (grrrr).
		painter.drawLine(fontMetrics().averageCharWidth() * longLinePos + 4, y(),
				 fontMetrics().averageCharWidth() * longLinePos + 4, height());
	}

	painter.end();

	Editor::paintEvent(event);
}

/**
  * Moves the line number widget into the TextEdit margin. We are not using layouts because
  * then the editor will be in another containing widget which will be nasty to access from WeaveEdit's
  * MainWindow.
  */
void TextEditor::resizeEvent(QResizeEvent *event)
{
	Editor::resizeEvent(event);

	// Update line numbers.
	if (showLineNumbers)
		lineNumberArea->setGeometry(QRect(contentsRect().left(), contentsRect().top(),
						  updateLineNumberAreaWidth(), contentsRect().height()));

	// Move the completion widget.
	if (completer && completer->isVisible())
		completer->movePopup();
}

/**
  * Move the completion widget.
  */
void TextEditor::moveEvent(QMoveEvent *event)
{
	Editor::moveEvent(event);

	if (completer && completer->isVisible())
		completer->movePopup();
}

/**
  * Handle key presses, which may be handled by TextEditor, Editor or the completion widget.
  */
void TextEditor::keyPressEvent(QKeyEvent *event)
{
	// Implement indent feature on tab.
	if (event->key() == Qt::Key_Tab && textCursor().hasSelection())
		indent();

	else
	{
		// Match the char input (eg: < with >).
		if (charMatchingEnabled)
			matchOpeningChar(event);

		// Let the completer handle the request.
		if (completer)
			completer->processKeyEvent(event);

		else
			Editor::keyPressEvent(event);

		// New line pressed, so process it for automatic indentation.
		if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)
			emit pressedNewLine();
	}
}

/**
  * If we click away from the completer widget, close it down.
  */
void TextEditor::mousePressEvent(QMouseEvent *event)
{
	if (completer)
		completer->hide();

	Editor::mouseMoveEvent(event);
}

/**
  * Paste only html and text data (as text because this is a plain text doc).
  */
void TextEditor::insertFromMimeData(const QMimeData *source)
{
	if (source->hasHtml() || source->hasText())
		textCursor().insertText(source->text());
}

/**
  * Move the completer popup when the window is moved.
  */
void TextEditor::windowMoved()
{
	if (completer && completer->isVisible())
		completer->movePopup();
}

/**
  * Get the word we need to complete.
  */
QString TextEditor::getWordToComplete() const
{
	QTextCursor cursor = textCursor();
	cursor.select(QTextCursor::WordUnderCursor);

	return cursor.selectedText();
}

/**
  * Repaints line numbers for visible lines (blocks) of text.
  */
void TextEditor::paintLineNumbers(QPaintEvent *event)
{
	int contentsY = verticalScrollBar()->value();
	int pageBottom = contentsY + viewport()->height();
	int lineCount = 0;
	QTextBlock block = document()->begin();
	QPainter painter(lineNumberArea);
	QPointF position;

	painter.fillRect(event->rect(), Qt::lightGray);

	// Draw each line number for each valid block.
	while (block.isValid())
	{
		lineCount++;
		position = document()->documentLayout()->blockBoundingRect(block).topLeft();

		// Break when all visible blocks have line numbers drawn.
		if (position.y() > pageBottom)
			break;

		painter.drawText(0, position.y() - contentsY + fontMetrics().ascent(), QString::number(lineCount));

		block = block.next();
	}

	painter.end();
}

/**
  * Set the showing of line numbers.
  */
void TextEditor::setLineNumbers(const bool show)
{
	if (show)
	{
		showLineNumbers = true;

		lineNumberArea->show();

		updateLineNumberAreaWidth();
	}

	else
	{
		showLineNumbers = false;

		lineNumberArea->hide();

		setViewportMargins(0, 0, 0, 0);
	}
}

/**
  * Gets the matching character. eg: < with >, ( with ) etc.
  */
QChar TextEditor::getOppositeChar(const QChar ch) const
{
	if (ch.unicode() == Qt::Key_ParenLeft)
		return Qt::Key_ParenRight;

	else if (ch.unicode() == Qt::Key_BraceLeft)
		return Qt::Key_BraceRight;

	else if (ch.unicode() == Qt::Key_BracketLeft)
		return Qt::Key_BracketRight;

	else if (ch.toLatin1() == Qt::Key_QuoteDbl)
		return Qt::Key_QuoteDbl;

	else if (ch.unicode() == Qt::Key_Apostrophe)
		return Qt::Key_Apostrophe;

	else if (ch.unicode() == Qt::Key_Less)
		return Qt::Key_Greater;

	else
		return QChar::Null;
}

/**
  * Test if the character has an opposite.
  */
bool TextEditor::hasOppositeChar(const QChar ch) const
{
	return !getOppositeChar(ch).isNull();
}

/**
  * Match the opening character and insert it's opposite number.
  */
void TextEditor::matchOpeningChar(QKeyEvent *event)
{
	bool matchChar = true;
	QTextCursor cursor = textCursor();

	if (event->key() == Qt::Key_Backspace)
	{
		if (hasOppositeChar(document()->characterAt(cursor.position() - 1)))
		{
			cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor);

			setTextCursor(cursor);

			Editor::keyPressEvent(event);
		}

		matchChar = false;
	}

	// The event->text().isEmpty is a hack, because meta key is represented as " by QChar.
	// but text() will only have " if the " key is pressed, not for meta.
	else if (!event->text().isEmpty() && hasOppositeChar(event->key()))
		insertPlainText(QString(getOppositeChar(event->key())));

	else
		matchChar = false;

	if (matchChar)
	{
		cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor);

		setTextCursor(cursor);
	}
}

/**
  * Set the current programming language from the GUI.
  */
void TextEditor::changeProgrammingLanguage(QAction *langAction)
{
	if (isVisible())
	{
		ProgrammingLanguage lang;

		if (langAction->text() == "C/C++")
			lang = TextEditor::C_CPLUSPLUS;

		else if (langAction->text() == "Python")
			lang = TextEditor::PYTHON;

		else if (langAction->text() == "Sh/Bash")
			lang = TextEditor::SH_BASH;

		else if (langAction->text() == "Java")
			lang = TextEditor::JAVA;

		else if (langAction->text() == "HTML")
			lang = TextEditor::HTML;

		else if (langAction->text() == "JavaScript")
			lang = TextEditor::JAVASCRIPT;

		else if (langAction->text() == "C#")
			lang = TextEditor::CSHARP;

		else if (langAction->text() == "Fortran")
			lang = TextEditor::FORTRAN;

		else
			lang = TextEditor::NONE;

		setProgrammingLanguage(lang);
	}
}

/**
  * Set the programming language to implement automatic indentation and syntax highlighting for.
  */
void TextEditor::setProgrammingLanguage(const ProgrammingLanguage newLang)
{
	programmingLanguage = newLang;

	if (indenter)
		delete indenter;

	if (newLang == C_CPLUSPLUS)
	{
		indenter = new CStyleIndenter(this);
		highlighter->init("cpp.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == CSHARP)
	{
		indenter = new CStyleIndenter(this);
		highlighter->init("csharp.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == JAVA)
	{
		indenter = new CStyleIndenter(this);
		highlighter->init("java.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == JAVASCRIPT)
	{
		indenter = new CStyleIndenter(this);
		highlighter->init("javascript.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == PYTHON)
	{
		indenter = new PythonIndenter(this);
		highlighter->init("python.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == HTML)
	{
		indenter = new HTMLIndenter(this);
		highlighter->init("html.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == SH_BASH)
	{
		indenter = new ShBashIndenter(this);
		highlighter->init("sh.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else if (newLang == FORTRAN)
	{
		indenter = new FortranIndenter(this);
		highlighter->init("fortran.lang");
		highlighter->toggleSyntaxHighlighting(true);
	}
	else
	{
		indenter = NULL;
		highlighter->toggleSyntaxHighlighting(false);
	}
}

/**
  * Save current document to file.
  */
void TextEditor::saveFile()
{
	file.open(QIODevice::WriteOnly);
	file.write(toPlainText().toLatin1());

	WelcomeScreen::getInstance()->addRecentFile(file.fileName(), TEXT);
}

/**
  * Called whenever the preferences are changed by the user, and updates the text editor to conform to them
  */
void TextEditor::preferencesChanged()
{
	//set the right margin
	if (Preferences::getInstance()->getValue(Preferences::SETTING_LONG_LINE_MARKER).toBool())
		longLinePos = Preferences::getInstance()->getValue(Preferences::SETTING_LONG_LINE_MARKER_POSITION).toInt();
	else
		longLinePos = -1;

	//set the autocompleter
	if (Preferences::getInstance()->getValue(Preferences::SETTING_AUTO_COMPLETION).toBool())
	{
		completer = new AutoCompletion(this);
	}
	else
	{
		if (completer != NULL)
			delete completer;

		completer = NULL;
	}

	//set the font
	QFont newDefaultFont;

	if (!Preferences::getInstance()->getValue(Preferences::SETTING_USE_SYSTEM_TEXT_FONT).toBool())
	{
		newDefaultFont = Preferences::getInstance()->getValue(Preferences::SETTING_DEFAULT_TEXT_FONT).value<QFont>();
		newDefaultFont.setPointSize(Preferences::getInstance()->getValue(Preferences::SETTING_DEFAULT_TEXT_FONT_SIZE).toInt());
	}
	else
	{
		newDefaultFont = QFont("Monospace");
		newDefaultFont.setPointSize(DEFAULT_FONT_SIZE);
	}

	document()->setDefaultFont(newDefaultFont);
	setFont(newDefaultFont);

	//set the tab width
	QFontMetrics metric(newDefaultFont);
	setTabStopWidth(metric.averageCharWidth() * (Preferences::getInstance()->getValue(Preferences::SETTING_TAB_WIDTH).toInt()));

	//toggle character matching
	charMatchingEnabled = Preferences::getInstance()->getValue(Preferences::SETTING_CHARACTER_MATCHING).toBool();

	//repaint the document to make sure it is up-to-date
	repaint();
}

/**
  * Saves the current document out to a file given a name.
  */
void TextEditor::saveFile(QString filename)
{
	QByteArray myBytes;
	QFile out(filename);
	out.open(QIODevice::WriteOnly);
	this->saveFile(myBytes);
	out.write(myBytes);
	out.close();
}

void TextEditor::saveFile(QByteArray& myBytes)
{
	myBytes=toPlainText().toLatin1();
}

/**
  * Save session information to file.
  */
QString TextEditor::saveSession(QString filename)
{
    QString line = "";
    //Call base class
     line = this->Editor::saveSession(filename);

    //Save formattedEditor specific variables
	//programmingLanguage
     line += ":" + QString::number(programmingLanguage) +":0";      //0 for text type editor

     return line;
}


void TextEditor::loadFile(QString filename)
{
    QByteArray myBytes;
    QFile file(filename);
    if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
	return;

    myBytes = file.readAll();	//Read in from file
    this->loadFile(myBytes);	//Set in the document

    file.close();
}

void TextEditor::loadFile(QByteArray myBytes)
{
    bool prev = document()->blockSignals(true);
    setText(myBytes);    //Add data back to the document
    document()->blockSignals(prev);
}

void TextEditor::loadSession(QString filename, QStringList metadata)
{
    Editor::loadSession(filename, metadata);    //Call base class load

    QString temp = metadata.at(3).toLocal8Bit().constData();

    switch(temp.toInt()){

	case 0: programmingLanguage = NONE;
	    break;
	case 1: programmingLanguage = C_CPLUSPLUS;
	    break;
	case 2: programmingLanguage = PYTHON;
	    break;
	case 3: programmingLanguage = SH_BASH;
	    break;
	case 4: programmingLanguage = JAVA;
	    break;
	case 5: programmingLanguage = HTML;
	    break;
	case 6: programmingLanguage = JAVASCRIPT;
	    break;
	case 7: programmingLanguage = CSHARP;
	    break;
	case 8: programmingLanguage = FORTRAN;
	    break;
	default: programmingLanguage = NONE;
    }
}

void TextEditor::setMeta(QStringList metadata)
{
    QString temp = metadata.at(3).toLocal8Bit().constData();

    switch(temp.toInt()){

	case 0: programmingLanguage = NONE;
	    break;
	case 1: programmingLanguage = C_CPLUSPLUS;
	    break;
	case 2: programmingLanguage = PYTHON;
	    break;
	case 3: programmingLanguage = SH_BASH;
	    break;
	case 4: programmingLanguage = JAVA;
	    break;
	case 5: programmingLanguage = HTML;
	    break;
	case 6: programmingLanguage = JAVASCRIPT;
	    break;
	case 7: programmingLanguage = CSHARP;
	    break;
	case 8: programmingLanguage = FORTRAN;
	    break;
	default: programmingLanguage = NONE;
    }
    Editor::setMeta(metadata);
}

QString TextEditor::getMeta()
{
    QString line="";

    line = this->Editor::getMeta();

    line += ":" + QString::number(programmingLanguage) +":0\n";      //0 for text type editor
    return line;
}
